﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="DataTableComparer.cs" company="">
//   
// </copyright>
// <summary>
//   The data table comparer.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace UITest.Helpers.TableCompare
{
    using System;
    using System.Collections.Generic;
    using System.Data;
    using System.Globalization;
    using System.Linq;
    using System.Text;

    /// <summary>
    ///     The data table comparer.
    /// </summary>
    public class DataTableComparer : IDataTableComparer
    {
        #region Fields

        private readonly int diffLimit;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        ///     Initializes a new instance of the <see cref="DataTableComparer" /> class.
        /// </summary>
        public DataTableComparer()
            : this(0)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DataTableComparer"/> class.
        ///     Creates a new instance of <see cref="DataTableComparer"/> class with a given diff limit
        /// </summary>
        /// <param name="diffLimit">
        /// Positive integer number representing the limit. When this limit is reached, the comparison
        ///     returns with the found differences and does not continue. 0 or negative number disables the limit.
        /// </param>
        public DataTableComparer(int diffLimit)
        {
            this.diffLimit = diffLimit;
        }

        #endregion

        #region Public Methods and Operators

        /// <summary>
        /// Compares two data tables cell by cell, excluding the columns specified in the exclude list
        /// </summary>
        /// <param name="leftTable">
        /// The DataTable object on the left side
        /// </param>
        /// <param name="rightTable">
        /// The DataTable object on the right side
        /// </param>
        /// <param name="columnsToExclude">
        /// The names of the columns to exclude from the comparison
        /// </param>
        /// <returns>
        /// <see cref="DataCompareResult"/> object which contains the differences (if any) and the formatted messages
        ///     about the result of the compare
        /// </returns>
        public DataCompareResult Compare(
            DataTable leftTable, 
            DataTable rightTable, 
            ICollection<string> columnsToExclude = null)
        {
            if (leftTable == null)
            {
                throw new ArgumentNullException("leftTable");
            }

            if (rightTable == null)
            {
                throw new ArgumentNullException("rightTable");
            }

            if (leftTable.Columns.Count == 0 || rightTable.Columns.Count == 0)
            {
                throw new InvalidOperationException("One (or all) of the compared tables has zero columns");
            }

            int diffCount = 0;

            bool isSchemaMatching = this.SchemaEquals(leftTable, rightTable, columnsToExclude);

            if (!isSchemaMatching)
            {
                return new DataCompareResult(false, "Schema mismatch detected between the tables.");
            }

            if (leftTable.Rows.Count != rightTable.Rows.Count)
            {
                return new DataCompareResult(false, "The tables have different number of rows.");
            }

            IEnumerable<DataColumn> examinedColumns =
                leftTable.Columns.OfType<DataColumn>()
                    .Where(c => columnsToExclude == null || !columnsToExclude.Contains(c.ColumnName));

            var differences = new List<DataTableDifference>();

            for (int i = 0; (i < leftTable.Rows.Count) && (diffCount < this.diffLimit || this.diffLimit <= 0); i++)
            {
                foreach (DataColumn column in examinedColumns)
                {
                    object leftValue = leftTable.Rows[i][column.ColumnName];
                    object rightValue = rightTable.Rows[i][column.ColumnName];

                    if (this.ValueEquals(leftValue, rightValue))
                    {
                        continue;
                    }

                    if (diffCount < this.diffLimit || this.diffLimit <= 0)
                    {
                        differences.Add(
                            new DataTableDifference
                                {
                                    LeftValue = leftValue, 
                                    RightValue = rightValue, 
                                    ColumnName = column.ColumnName, 
                                    RowNumber = i + 1
                                });
                        diffCount++;
                    }
                    else
                    {
                        break;
                    }
                }
            }

            string messages = this.GenerateDiffMessages(differences);

            if (diffCount >= this.diffLimit && this.diffLimit > 0)
            {
                messages = string.Concat(
                    messages, 
                    "\n", 
                    "The diff limit of ", 
                    this.diffLimit.ToString(CultureInfo.InvariantCulture), 
                    " is reached, comparison halted.");
            }

            return new DataCompareResult(diffCount == 0, messages, differences);
        }

        #endregion

        #region Methods

        /// <summary>
        /// Generates messages for the returned <see cref="DataCompareResult"/> object based on the list of differences found.
        /// </summary>
        /// <param name="diffList">
        /// The list of differences
        /// </param>
        /// <returns>
        /// The formatted messages about the found differences separated by new lines.
        /// </returns>
        private string GenerateDiffMessages(IEnumerable<DataTableDifference> diffList)
        {
            if (diffList == null)
            {
                throw new ArgumentNullException("diffList");
            }

            if (!diffList.Any())
            {
                return "No differences found.";
            }

            var stringBuilder = new StringBuilder();
            foreach (DataTableDifference diff in diffList.OrderBy(x => x.RowNumber))
            {
                stringBuilder.AppendLine(
                    string.Format(
                        "Difference in Row {0} - Column [{1}]: {2} -> {3}; ", 
                        diff.RowNumber, 
                        diff.ColumnName, 
                        diff.LeftValue, 
                        diff.RightValue));
            }

            return stringBuilder.ToString();
        }

        /// <summary>
        /// Compares the column specification of two DataTable objects based on a custom
        ///     <see cref="DataColumnEqualityComparer"/> class.
        /// </summary>
        /// <param name="leftTable">
        /// List of column names which should be excluded from the comparison
        /// </param>
        /// <param name="rightTable">
        /// The left Table.
        /// </param>
        /// <param name="columnsToExclude">
        /// The left table of the comparison
        /// </param>
        /// <param name="columnsToExclude">
        /// The right table of the comparison
        /// </param>
        /// <returns>
        /// Boolean value on whether the two table's schema are equal
        /// </returns>
        private bool SchemaEquals(
            DataTable leftTable, 
            DataTable rightTable, 
            ICollection<string> columnsToExclude = null)
        {
            if (leftTable == null)
            {
                throw new ArgumentNullException("leftTable");
            }

            if (rightTable == null)
            {
                throw new ArgumentNullException("rightTable");
            }

            IEnumerable<DataColumn> leftColumns =
                leftTable.Columns.OfType<DataColumn>()
                    .Where(column => columnsToExclude == null || !columnsToExclude.Contains(column.ColumnName));

            IEnumerable<DataColumn> rightColumns =
                rightTable.Columns.OfType<DataColumn>()
                    .Where(column => columnsToExclude == null || !columnsToExclude.Contains(column.ColumnName));

            if (leftColumns.Count() != rightColumns.Count())
            {
                return false;
            }

            return !leftColumns.Except(rightColumns, DataColumnEqualityComparer.Instance).Any();
        }

        /// <summary>
        /// Cell value compare logic. Currently based on the string representations of the values.
        /// </summary>
        /// <param name="leftValue">
        /// The value of the cell from the left side table
        /// </param>
        /// <param name="rightValue">
        /// The value of the cell from the right side table
        /// </param>
        /// <returns>
        /// Boolean value on whether the two values are equal
        /// </returns>
        private bool ValueEquals(object leftValue, object rightValue)
        {
            return string.Equals(leftValue.ToString(), rightValue.ToString());
        }

        #endregion
    }
}